/**
 * Copyright (c) <2010>, <SUNNY GUPTA>
 * All rights reserved.

 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   * Neither the name of the <ORGANIZATION> nor the
 *     names of its contributors may be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL <SUNNY GUPTA> BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.gwtp.investmentadvisor.client.gui.authentication;

import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.HasClickHandlers;
import com.google.gwt.event.dom.client.HasKeyPressHandlers;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.dom.client.KeyPressHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.user.client.ui.HasValue;
import com.google.inject.Inject;
import com.gwtp.investmentadvisor.client.dispatch.DispatchCallback;
import com.gwtp.investmentadvisor.client.gatekeeper.NotLoggedInGateKeeper;
import com.gwtp.investmentadvisor.client.place.PlaceNameTokens;
import com.gwtp.investmentadvisor.shared.action.LoginAttempt;
import com.gwtp.investmentadvisor.shared.action.LoginAttemptResult;
import com.gwtp.investmentadvisor.shared.entities.CurrentUser;
import com.gwtp.investmentadvisor.shared.entities.LoginInfo;
import com.gwtp.investmentadvisor.shared.event.LoginAttemptEvent;
import com.gwtp.investmentadvisor.shared.event.LoginAttemptEventHandler;
import com.gwtp.investmentadvisor.shared.event.LoginSuccessfulEvent;
import com.gwtplatform.dispatch.client.DispatchAsync;
import com.gwtplatform.mvp.client.EventBus;
import com.gwtplatform.mvp.client.Presenter;
import com.gwtplatform.mvp.client.annotations.NameToken;
import com.gwtplatform.mvp.client.annotations.ProxyCodeSplit;
import com.gwtplatform.mvp.client.annotations.UseGatekeeper;
import com.gwtplatform.mvp.client.proxy.PlaceManager;
import com.gwtplatform.mvp.client.proxy.RevealRootContentEvent;

/**
 * 
 * @author Sunny Gupta
 *
 */
public class LoginScreenPresenter 
    extends Presenter<LoginScreenPresenter.Display, LoginScreenPresenter.Proxy> {

  protected static final String INTERNAL_ERROR_MESSAGE = 
      "An internal error occured on the server while attempting to login. Pls try again after some time";

  // Cached Dispatcher
  private final DispatchAsync dispatcher;
  
  // Flag to indicate to value change handlers to ingore the changes
  private boolean ignoreValueChanges;
  
  private final CurrentUser currentUser;

  private final PlaceManager placeManager;

  /**
   * 
   * @author Sunny Gupta
   *
   */
  public interface Display extends com.gwtplatform.mvp.client.View {
    // For login button
    HasClickHandlers getSubmitButton();
    // For username text
    HasValue<String> getUsernameText();
    // For password text
    HasValue<String> getPasswordText();
    // For remember flag
    HasValue<Boolean> getRememberMeFlag();
    
    // For keypress event handling
    HasKeyPressHandlers getUsernameKeyPressGenerator();
    HasKeyPressHandlers getPasswordKeyPressGenerator();
    
    // Configure error messages
    void configureErrorMessages(String usernameErrorMessage, String passwordErrorMessage);
    
    // Configure values. It may be required for initialization
    void configureValues(String username, String password, boolean rememberMeFlag);
    
    // Raise error if username is invalid
    void raiseUsernameError();
    // Raise error if password is invalid
    void raisePasswordError();
    
    // Disable login button
    void disableLoginButton();
    
    // Disable login button
    void enableLoginButton();
    
    // Clear errors
    void clearUsernameError();
    void clearPasswordError();
    
    void clearLoginAttemptMessage();
    
    // Clear fields
    void clearFields();
    
    // Login Attempt messages
    void setLoginAttemptSuccessMessage(String message);
    void setLoginAttemptFailureMessage(String message);
    
    // Set focus
    void focusUsernameText();
    void focusPasswordText();
  }
  
  /**
   * 
   * @author Sunny Gupta
   *
   */
  @ProxyCodeSplit
  @NameToken(PlaceNameTokens.LoginScreenNameToken)
  // Gate keeper so that it is not called when user is already logged in
  @UseGatekeeper(NotLoggedInGateKeeper.class)
  public interface Proxy extends 
    com.gwtplatform.mvp.client.proxy.ProxyPlace<LoginScreenPresenter> {
  }
  
  @Inject
  public LoginScreenPresenter(final Display view, final Proxy proxy,
      final EventBus eventBus, final DispatchAsync dispatcher,
      final PlaceManager placeManager, final CurrentUser currentUser) {
    super(eventBus, view, proxy);
    
    // Initialize dispatcher
    this.dispatcher = dispatcher;
    
    // Initialize place manager
    this.placeManager = placeManager;
    
    // Configure display error messages
    this.getView().configureErrorMessages(
        UserAuthenticationConstants.MINIMUM_USERNAME_LENGTH_ERROR_MESSAGE, 
        UserAuthenticationConstants.MINIMUM_PASSWORD_LENGTH_ERROR_MESSAGE);
    
    // Disable login button initially
    this.getView().disableLoginButton();
    
    // Enable value change handlers
    this.ignoreValueChanges = false;
    
    // Initialize currentuser
    this.currentUser = currentUser;
    
    // Do the binding
    bind();
  }

  @Override
  protected void onBind() {
    super.onBind();
    
    // Obtain view
    final Display view = getView();
    
    // Add Event Handlers
        
    /**
     * Handler for LoginAttemptEvent
     * We don't want to hard link event handling and rather follow standard pattern
     * It's just that response code also resides in the same presenter in this case
     */
    addRegisteredHandler(LoginAttemptEvent.TYPE, new LoginAttemptEventHandler() {
      
      @Override
      public void onLoginAttemptEventComplete(final LoginAttemptEvent event) {
        // TODO: Clear wait
        // display.stopProcessing();
        
        boolean status = event.getLoginAttemptResult().getLoginAuthenticationInfo().isAuthenticated();
        
        if (status) {
          // Ensure login info matches
          final LoginInfo loginInfo = event.getLoginAttemptResult().getLoginInfo();
          if (!loginInfo.getUserName().equals(view.getUsernameText().getValue())) {
            // Login name doesn't match, some problem
            status = false;
          }
        }
        
        if (status) {
          // Set success message
          view.setLoginAttemptSuccessMessage(UserAuthenticationConstants.LOGIN_SUCCESS_MESSAGE);
          
          // Reset Current User
          currentUser.resetCurrentUser(event.getLoginAttemptResult().getLoginAuthenticationInfo());
          currentUser.updateSessionCookie();
          
          // Raise login successful event
          final LoginSuccessfulEvent loginSuccessfulEvent = new LoginSuccessfulEvent(event.getLoginAttemptResult().getLoginInfo());
          // Obtain redirect token
          final String redirectHistoryToken = placeManager.getCurrentPlaceRequest().getParameter(PlaceNameTokens.LOGIN_PARAM_REDIRECT, null);
          loginSuccessfulEvent.setRedirectHistoryToken(redirectHistoryToken);
          getEventBus().fireEvent(loginSuccessfulEvent);
        } else {
          // Clear fields, while disabling value change handlers
          ignoreValueChanges = true;
          view.clearFields();
          ignoreValueChanges = false;

          // Set failure message
          view.setLoginAttemptFailureMessage(UserAuthenticationConstants.LOGIN_FAILURE_MESSAGE);
          
          // Focus username field
          view.focusUsernameText();
        }
      }
    });
    
    // Login Button
    registerHandler(view.getSubmitButton().addClickHandler(new ClickHandler() {
      
      @Override
      public void onClick(final ClickEvent event) {
        // TODO: Show wait
        // display.startProcessing();
        
        // Authenticate
        authenticateLoginAttempt();
      }
    }));
    
    // Username Text
    registerHandler(view.getUsernameText().addValueChangeHandler(new ValueChangeHandler<String>() {
      
      @Override
      public void onValueChange(final ValueChangeEvent<String> event) {
        // Ignore if flag set
        if (ignoreValueChanges) {
          return;
        }
        
        // Check for minimum length
        if (!isUsernameValid()) {
          view.raiseUsernameError();
          view.disableLoginButton();
        } else {
          view.clearUsernameError();
          if (isPasswordValid()) {
            view.enableLoginButton();
          }
        }
        // Clear any login message
        view.clearLoginAttemptMessage();
      }
    }));
    
    // This is required to clear error as soon as field becomes valid, without the need for focus loss
    registerHandler(view.getUsernameKeyPressGenerator().addKeyPressHandler(new KeyPressHandler() {
      
      @Override
      public void onKeyPress(final KeyPressEvent event) {
        /**
         * Check for minimum length. However, keypress event is generate BEFORE the value
         * changes, and hence we need to compare with MINLENGTH - 1
         */
        if (willUsernameBeValidAfterKeyPress()) {
          view.clearUsernameError();
          if (willPasswordBeValidAfterKeyPress()) {
            view.enableLoginButton();
          }
        } else {
          // Disable login as user can use mouse to click
          view.disableLoginButton();
        }
        // Clear any login message
        view.clearLoginAttemptMessage();
      }
    }));
    
    // Password Text
    registerHandler(view.getPasswordText().addValueChangeHandler(new ValueChangeHandler<String>() {
      
      @Override
      public void onValueChange(final ValueChangeEvent<String> event) {
        // Ignore if flag set
        if (ignoreValueChanges) {
          return;
        }
        
        // Check for minimum length
        if (!isPasswordValid()) {
          view.raisePasswordError();
          view.disableLoginButton();
        } else {
          view.clearPasswordError();
          if (isUsernameValid()) {
            view.enableLoginButton();
          }
        }
        // Clear any login message
        view.clearLoginAttemptMessage();
      }
    }));
    
    // This is required to clear error as soon as field becomes valid, without the need for focus loss
    registerHandler(view.getPasswordKeyPressGenerator().addKeyPressHandler(new KeyPressHandler() {
      
      @Override
      public void onKeyPress(final KeyPressEvent event) {
        /**
         * Check for minimum length. However, keypress event is generate BEFORE the value
         * changes, and hence we need to compare with MINLENGTH - 1
         */
        if (willPasswordBeValidAfterKeyPress()) {
          view.clearPasswordError();
          if (willUsernameBeValidAfterKeyPress()) {
            view.enableLoginButton();
          }
        } else {
          // Disable login as user can use mouse to click
          view.disableLoginButton();
        }
        // Clear any login message
        view.clearLoginAttemptMessage();
      }
    }));
    
  };

  protected void authenticateLoginAttempt() {
    // Obtain view
    final Display view = getView();
    
    // Log.info("Authenticating Login Info");
    
    // Dispatch action
    dispatcher.execute(new LoginAttempt(new LoginInfo(view.getUsernameText().getValue(),
          view.getPasswordText().getValue())), 
        new DispatchCallback<LoginAttemptResult>() {

          @Override
          public void callback(final LoginAttemptResult result) {
            // Take the result from the server and notify client interested components
            fireEvent(new LoginAttemptEvent(result));
          }
          
          @Override
          public void callbackError(final Throwable t) {
            view.setLoginAttemptFailureMessage(INTERNAL_ERROR_MESSAGE);
          }
    });
  }

  protected boolean willPasswordBeValidAfterKeyPress() {
    return getView().getPasswordText().getValue().length() >= UserAuthenticationConstants.MINIMUM_PASSWORD_LENGTH - 1;
  }

  protected boolean willUsernameBeValidAfterKeyPress() {
    return getView().getUsernameText().getValue().length() >= UserAuthenticationConstants.MINIMUM_USERNAME_LENGTH - 1;
  }

  private boolean isUsernameValid() {
    return getView().getUsernameText().getValue().length() >= UserAuthenticationConstants.MINIMUM_USERNAME_LENGTH;
  }
  
  private boolean isPasswordValid() {
    return getView().getPasswordText().getValue().length() >= UserAuthenticationConstants.MINIMUM_PASSWORD_LENGTH;
  }

  @Override
  protected void revealInParent() {
    RevealRootContentEvent.fire(this, this);
  }

}
